﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;

namespace OOLib
{
   /// <summary>
   /// Base wrapper class for COM objects.
   /// </summary>
   public class ComObjectWrapper : IDisposable, INotifyPropertyChanged
   {
      #region Constructors

      /// <summary>
      /// Constructor
      /// </summary>
      /// <remarks>
      /// After using this constructor, the ComObj must be set manually!
      /// </remarks>
      public ComObjectWrapper()
      { }

      /// <summary>
      /// Constructor
      /// </summary>
      /// <param name="comObject">COM object</param>
      public ComObjectWrapper(object comObject)
      {
         ComObj = comObject;
      }

      #endregion 

      private object _ComObj = null;

      /// <summary>
      /// Gets the COM object.
      /// </summary>
      public object ComObj
      {
         get { return _ComObj; }
         internal set
         {
            _ComObj = value;
            if (_ComObj != null)
            {
               OnComObjSet();
            }
         }
      }

      /// <summary>
      /// Is called when the ComObj is finally set.
      /// </summary>
      protected virtual void OnComObjSet()
      {
         lock (_ComObjects)
         {
            _ComObjects.Add(new WeakReference(this));
         }
      }

      /// <summary>
      /// Compares the wrapped COM object with an other
      /// </summary>
      /// <param name="other">Other com object.</param>
      /// <returns>Return true if the COM objects are the same, otherwise false</returns>
      /// <remarks>
      /// The base implementation compares the COM objects by Object.ReferenceEquals().
      /// </remarks>
      public virtual bool EqualsComObject(object other)
      {
         return Object.ReferenceEquals(ComObj, other);
      }

      private static List<WeakReference> _ComObjects = new List<WeakReference>();

      /// <summary>
      /// Gets the warpper object for the given COM object.
      /// </summary>
      /// <typeparam name="T">Type of the desired wrapper object.</typeparam>
      /// <param name="comObject">COM object to wrap</param>
      /// <returns>Returns the COM object wrapper.</returns>
      /// <remarks>
      /// If there is already a wrapper object for the given COM object then the existion one is returned.
      /// Else a new one is created.
      /// </remarks>
      public static T GetObjectWrapper<T>(object comObject) where T : ComObjectWrapper, new()
      {
         lock (_ComObjects)
         {
            for (int i = _ComObjects.Count - 1; i >= 0; i--)
            {
               if (!_ComObjects[i].IsAlive)
               {
                  _ComObjects.RemoveAt(i);
               }
               else
               {
                  var obj = _ComObjects[i].Target as T;
                  if (obj != null && obj.EqualsComObject(comObject))
                  {
                     return obj;
                  }
               }
            }
         }
         if (comObject != null)
         {
            return new T()
               {
                  ComObj = comObject
               };
         }
         return null;
      }

      /// <summary>
      /// Invokes an COM method on this object.
      /// </summary>
      /// <param name="name">Method name.</param>
      /// <param name="args">Method arguments.</param>
      /// <returns>Returns the return value of the method, or null if method has no return value.</returns>
      public object InvokeMethod(string name, params object[] args)
      {
         return InvokeMethodOf(ComObj, name, args, null);
      }

      /// <summary>
      /// Invokes an COM method on this object.
      /// </summary>
      /// <param name="name">Method name.</param>
      /// <param name="args">Method arguments.</param>
      /// <param name="byRef">Array with a bool value for each argument. true means argument is by ref, false by value.</param>
      /// <returns>Returns the return value of the method, or null if method has no return value.</returns>
      public object InvokeMethod(string name, object[] args, bool[] byRef)
      {
         return InvokeMethodOf(ComObj, name, args, byRef);
      }

      /// <summary>
      /// Invokes an COM method on the target object.
      /// </summary>
      /// <param name="target">Target COM object.</param>
      /// <param name="name">Method name.</param>
      /// <param name="args">Method arguments.</param>
      /// <returns>Returns the return value of the method, or null if method has no return value.</returns>
      public static object InvokeMethodOf(object target, string name, params object[] args)
      {
         return InvokeMethodOf(target, name, args, null);
      }

      /// <summary>
      /// Invokes an COM method on the target object.
      /// </summary>
      /// <param name="target">Target COM object.</param>
      /// <param name="name">Method name.</param>
      /// <param name="args">Method arguments.</param>
      /// /// <param name="byRef">Array with a bool value for each argument. true means argument is by ref, false by value.</param>
      /// <returns>Returns the return value of the method, or null if method has no return value.</returns>
      public static object InvokeMethodOf(object target, string name, object[] args, bool[] byRef)
      {
         if (args == null)
         {
            args = new object[] { };
         }

         ParameterModifier[] modifiers = null;
         if (byRef != null)
         {
            var pm = new ParameterModifier(args.Length);

            for (int n = 0; n < args.Length; ++n)
            {
               pm[n] = n < byRef.Length && byRef[n];
            }
            modifiers = new ParameterModifier[] { pm };
         }
         return target.GetType().InvokeMember(name, BindingFlags.InvokeMethod, null, target, args, modifiers, null, null);
      }

      /// <summary>
      /// Sets a value for a COM Property on this object.
      /// </summary>
      /// <param name="name">Property name.</param>
      /// <param name="value">New value.</param>
      public void SetProperty(string name, object value)
      {
         SetPropertyOf(ComObj, name, value);
      }

      /// <summary>
      /// Sets a value for a COM Property on the target object.
      /// </summary>
      /// <param name="target">Target COM object.</param>
      /// <param name="name">Property name.</param>
      /// <param name="value">New value.</param>
      public static void SetPropertyOf(object target, string name, object value)
      {
         target.GetType().InvokeMember(name, BindingFlags.SetProperty, null, target, new object[] { value });
      }

      /// <summary>
      /// Sets the current value for a COM Property on this object.
      /// </summary>
      /// <param name="name">Property name.</param>
      /// <returns>Return the current value of the COM property.</returns>
      public object GetProperty(string name)
      {
         return GetPropertyOf(ComObj, name);
      }

      /// <summary>
      /// Sets the current value for a COM Property on the target object.
      /// </summary>
      /// <param name="target">Target COM object.</param>
      /// <param name="name">Property name.</param>
      /// <returns>Return the current value of the COM property.</returns>
      public static object GetPropertyOf(object target, string name)
      {
         return target.GetType().InvokeMember(name, BindingFlags.GetProperty, null, target, null);
      }

      /// <summary>
      /// Converts a file name into a url.
      /// </summary>
      /// <param name="fileName">File name</param>
      /// <returns>Returns the url of the given file name.</returns>
      public static string FileNameToUrl(string fileName)
      {
         return String.Format("file:///{0}", fileName.Replace('\\', '/'));
      }

      /// <summary>
      /// Raises an PropertyChanged event for the given property.
      /// </summary>
      /// <param name="propertyName">Property name</param>
      protected virtual void NotifyPropertyChanged(string propertyName)
      {
         var temp = PropertyChanged;
         if (temp != null)
         {
            temp(this, new PropertyChangedEventArgs(propertyName));
         }
      }

      /// <summary>
      /// Occures when the value of an property has changed.
      /// </summary>
      public event PropertyChangedEventHandler PropertyChanged;

      /// <summary>
      /// Checks if the object is disposed and throws an ObjectDisposedException if this is the case.
      /// </summary>
      public virtual void CheckDisposed()
      {
         if (IsDisposed)
         {
            throw new ObjectDisposedException(GetType().Name);
         }
      }

      /// <summary>
      /// Gets if the object is disposed.
      /// </summary>
      /// <remarks>
      /// If the object is created by the parameterless constructor, the IsDisposed returns true 
      /// until the property ComObj is set!
      /// </remarks>
      public bool IsDisposed
      {
         get { return ComObj == null; }
      }

      /// <summary>
      /// Disposes the object.
      /// </summary>
      /// <remarks>
      /// This is the .Net Dispose() and will onyl dispose the wrapper.
      /// Call OODispose() to dispose the OpenOffice object.
      /// </remarks>
      public void Dispose()
      {
         Dispose(true);
         GC.SuppressFinalize(this);
      }

      /// <summary>
      /// Destructor
      /// </summary>
      ~ComObjectWrapper()
      {
         Dispose(false);
      }

      /// <summary>
      /// Disposes the object
      /// </summary>
      /// <param name="disposing">true if this is called from disposing, false if called from destructor.</param>
      protected virtual void Dispose(bool disposing)
      {
         ComObj = null;
         lock (_ComObjects)
         {
            for (int i = _ComObjects.Count - 1; i >= 0; i--)
            {
               if (Object.ReferenceEquals(this, _ComObjects[i].Target))
               {
                  _ComObjects.RemoveAt(i);
                  break;
               }
            }
         }
      }
   }
}
